﻿using Microsoft.SPOT.Hardware;
using System.Threading;
namespace MicroFrameworkDriverLibrary.Devices
{
    using System;
    using Microsoft.SPOT.Hardware;
    using MicroFrameworkDriverLibrary.Interfaces;

    /// <summary>
    /// MCP23017 - i2c 16 input/output port expander
    /// 
    /// Use this chip from 2.7-5.5V (good for any 3.3V or 5V setup), and you can sink/source up to 20mA from any of the I/O pins.
    /// </summary>
    /// <remarks><![CDATA[
    /// MCP23017 (N) DIP28 pin layout:
    /// 
    ///   28 27 26 25 24 23 22 21 20 19 18 17 16 15
    ///   │  │  │  │  │  │  │  │  │  │  │  │  │  │
    /// ███████████████████████████████████████████
    /// ▀██████████████████████████████████████████
    ///   █████████████████████████████████████████
    /// ▄██████████████████████████████████████████
    /// ███████████████████████████████████████████
    ///   │  │  │  │  │  │  │  │  │  │  │  │  │  │
    ///   1  2  3  4  5  6  7  8  9  10 11 12 13 14
    /// 
    /// 1   GPBO                                28 GPA7
    /// 2   GPB1                                27 GPA6
    /// 3   GPB2                                26 GPA5
    /// 4   GPB3                                25 GPA4
    /// 5   GPB4                                24 GPA3 
    /// 6   GPB5                                23 GPA2 
    /// 7   GPB6                                22 GPA1 
    /// 8   GPB7                                21 GPA0
    /// 9   VDD  (3.3v)                         20 INTA
    /// 10  VSS  (GND)                          19 INTB
    /// 11  NC                                  18 RESET (+Pullup Required)
    /// 12  SCL  [See Note] (+Pullup Required)  17 A2   [See Note]
    /// 13  SDA  [See Note] (+Pullup Required)  16 A1   [See Note]
    /// 14  NC                                  15 A0   [See Note]
    /// 
    /// NOTE:
    ///   SCL: Netdiono Pin 4, FEZ Cobra Pin IO12
    ///   SDA: Netduino Pin 5, FEZ Cobra Pin IO11
    ///   
    ///   Hardware Address: [0 = GND, 1 = +Pullup)
    ///     A0/A1/A2
    ///     --------
    ///     0  0  0   = 0x20
    ///     1  0  0   = 0x21
    ///     0  1  0   = 0x22
    ///     1  1  0   = 0x23
    ///     0  0  1   = 0x24
    ///     1  0  1   = 0x25
    ///     0  1  1   = 0x26
    ///     1  1  1   = 0x27
    /// 
    /// ]]></remarks>
    public sealed class MCP23017 : I2CDeviceHelper
    {
        private const ushort HARDWARE_BASE_ADDRESS = 0x20;

        private const byte MCP23017_IODIRA = 0x00;
        private const byte MCP23017_IPOLA = 0x02;
        private const byte MCP23017_GPINTENA = 0x04;
        private const byte MCP23017_DEFVALA = 0x06;
        private const byte MCP23017_INTCONA = 0x08;
        private const byte MCP23017_IOCONA = 0x0A;
        private const byte MCP23017_GPPUA = 0x0C;
        private const byte MCP23017_INTFA = 0x0E;
        private const byte MCP23017_INTCAPA = 0x10;
        private const byte MCP23017_GPIOA = 0x12;
        private const byte MCP23017_OLATA = 0x14;

        private const byte MCP23017_IODIRB = 0x01;
        private const byte MCP23017_IPOLB = 0x03;
        private const byte MCP23017_GPINTENB = 0x05;
        private const byte MCP23017_DEFVALB = 0x07;
        private const byte MCP23017_INTCONB = 0x09;
        private const byte MCP23017_IOCONB = 0x0B;
        private const byte MCP23017_GPPUB = 0x0D;
        private const byte MCP23017_INTFB = 0x0F;
        private const byte MCP23017_INTCAPB = 0x11;
        private const byte MCP23017_GPIOB = 0x13;
        private const byte MCP23017_OLATB = 0x15;

        private enum IODirection
        {
            Output = 0,
            Input = 1
        }

        private enum Polarity
        {
            Normal = 0,
            Invert = 1,
        }

        private IMCP23017DevicePort[] _devicePorts = new IMCP23017DevicePort[16];  // There are a total of 16 i/o ports that can be created for this driver.

        public MCP23017(I2CDevice sharedI2C, int clockRateKHz = 400, int transacitonTimeoutMs = 50, ushort hardwareAddress = HARDWARE_BASE_ADDRESS)
            : base(sharedI2C, hardwareAddress, clockRateKHz, transacitonTimeoutMs)
        {
        }

        public void InitializeDevice()
        {
            this.SwapConfiguration(() =>
            {
                this.WriteRegister(MCP23017_IODIRA, 0xFF);
                this.WriteRegister(MCP23017_IODIRB, 0xFF);

                this.WriteRegister(MCP23017_IPOLA, 0x00);
                this.WriteRegister(MCP23017_IPOLB, 0x00);

                this.WriteRegister(MCP23017_GPINTENA, 0x00);
                this.WriteRegister(MCP23017_GPINTENB, 0x00);

                this.WriteRegister(MCP23017_DEFVALA, 0x00);
                this.WriteRegister(MCP23017_DEFVALB, 0x00);

                this.WriteRegister(MCP23017_INTCONA, 0x00);
                this.WriteRegister(MCP23017_INTCONB, 0x00);

                // This register is shared accross both banks for some bits
                this.WriteRegister(MCP23017_IOCONA, 0x00);
                this.WriteRegister(MCP23017_IOCONB, 0x00);

                this.WriteRegister(MCP23017_GPPUA, 0x00);
                this.WriteRegister(MCP23017_GPPUB, 0x00);

                this.WriteRegister(MCP23017_INTFA, 0x00);
                this.WriteRegister(MCP23017_INTFB, 0x00);

                this.WriteRegister(MCP23017_INTCAPA, 0x00);
                this.WriteRegister(MCP23017_INTCAPB, 0x00);

                this.WriteRegister(MCP23017_GPIOA, 0x00);
                this.WriteRegister(MCP23017_GPIOB, 0x00);

                this.WriteRegister(MCP23017_OLATA, 0x00);
                this.WriteRegister(MCP23017_OLATB, 0x00);
            });
        }

        public IOutputPort CreateOutputPort(Cpu.Pin portId, bool initialState)
        {
            if (this._devicePorts[(int)portId] != null)
            {
                // This device port is already in use.
            }

            var port = new MCP23017OutputPort(this, portId, initialState);
            this._devicePorts[(int)portId] = port;
            return port;
        }

        protected override void Dispose(bool disposing)
        {
            base._disposed = true;
        }

        private void Write(Cpu.Pin pin, bool value)
        {
            var address = ((byte)pin < 8) ? MCP23017_GPIOA : MCP23017_GPIOB;
            pin = ((byte)pin < 8) ? pin : (Cpu.Pin)((byte)pin - 8);

            this.WriteRegister(address, pin, value);
        }

        private bool Read(Cpu.Pin pin)
        {
            var address = ((byte)pin < 8) ? MCP23017_GPIOA : MCP23017_GPIOB;
            pin = ((byte)pin < 8) ? pin : (Cpu.Pin)((byte)pin - 8);

            return this.ReadRegister(address, pin);
        }

        private void SetPinDirection(Cpu.Pin pin, IODirection direction)
        {
            var address = ((byte)pin < 8) ? MCP23017_IODIRA : MCP23017_IODIRB;
            pin = ((byte)pin < 8) ? pin : (Cpu.Pin)((byte)pin - 8);

            this.WriteRegister(address, pin, direction == IODirection.Input);
        }

        private void SetPinPolarity(Cpu.Pin pin, Polarity polarity)
        {
            var address = ((byte)pin < 8) ? MCP23017_GPINTENA : MCP23017_GPINTENB;
            pin = ((byte)pin < 8) ? pin : (Cpu.Pin)((byte)pin - 8);

            this.WriteRegister(address, pin, polarity == Polarity.Invert);
        }

        private void WriteRegister(byte register, byte value)
        {
            // This method assumes the the configuration has already been setup. So don't do it again.
            var result = this._sharedI2C.Execute(new I2CDevice.I2CTransaction[] 
            { 
                I2CDevice.CreateWriteTransaction(new byte[] { register, value })
            }, this._transactionTimeoutMs);
        }

        private void WriteRegister(byte register, Cpu.Pin pin, bool enable)
        {
            this.SwapConfiguration(() =>
            {
                // First read the current value
                byte currentValue = this.ReadRegister(register);

                // Set the pins contents to the new value
                pin = ((byte)pin < 8) ? pin : (Cpu.Pin)((byte)pin - 8);
                currentValue = SetBit(currentValue, (byte)pin, enable);

                // Write the new value
                this.WriteRegister(register, currentValue);
            });
        }

        private byte ReadRegister(byte register)
        {
            // This method assumes the the configuration has already been setup. So don't do it again.
            byte[] readBuffer = new byte[1];
            var result = this._sharedI2C.Execute(new I2CDevice.I2CTransaction[] 
            { 
                I2CDevice.CreateWriteTransaction(new byte[] { register }),
                I2CDevice.CreateReadTransaction(readBuffer)
            }, this._transactionTimeoutMs);

            return readBuffer[0];
        }

        private bool ReadRegister(byte register, Cpu.Pin pin)
        {
            bool result = false;
            this.SwapConfiguration(() =>
            {
                var currentValue = this.ReadRegister(register);

                pin = ((byte)pin < 8) ? pin : (Cpu.Pin)((byte)pin - 8);
                result = GetBit(currentValue, (byte)pin);
            });
            return result;
        }

        private interface IMCP23017DevicePort { }

        private sealed class MCP23017OutputPort : IMCP23017DevicePort, IOutputPort, IDisposable
        {

            public MCP23017OutputPort(MCP23017 owner, Cpu.Pin portId, bool initialState)
            {
                this.Owner = owner;
                this.Id = portId;
                this.InitialState = initialState;

                this.Owner.Write(this.Id, initialState);
                this.Owner.SetPinDirection(this.Id, IODirection.Output);
            }

            private MCP23017 Owner { get; set; }

            public Cpu.Pin Id
            {
                get;
                private set;
            }

            public bool InitialState
            {
                get;
                private set;
            }

            public bool Read()
            {
                return this.Owner.Read(this.Id);
            }

            public void Write(bool value)
            {
                this.Owner.Write(this.Id, value);
            }

            void IDisposable.Dispose()
            {
            }
        }

        private sealed class MCP23017InputPort : IMCP23017DevicePort, IInputPort, IDisposable
        {
            public MCP23017InputPort(MCP23017 owner, Cpu.Pin portId, bool glitchFilter, Port.ResistorMode resistorMode, bool normallyClosed = true)
            {
                this.Owner = owner;
                this.Id = portId;
                this.GlitchFilter = glitchFilter;
                this.Resistor = resistorMode;

                //this.Owner.Write(this.Id, initialState);
                this.Owner.SetPinDirection(this.Id, IODirection.Input);
            }

            private MCP23017 Owner { get; set; }

            public bool GlitchFilter
            {
                get
                {
                    throw new NotImplementedException();
                }
                set
                {
                    throw new NotImplementedException();
                }
            }

            public Cpu.Pin Id
            {
                get;
                private set;
            }

            public bool Read()
            {
                throw new NotImplementedException();
            }

            public Port.ResistorMode Resistor
            {
                get;
                set;
            }

            void IDisposable.Dispose()
            {
            }

            public void DisableInterrupt()
            {
            }

            public void EnableInterrupt()
            {
            }
        }
    }
}
